package com.wx.v3;

import com.motu.vertx.module.utility.base.BaseRedisKey;
import com.motu.vertx.module.utility.redis.RedisTools;
import com.motu.vertx.module.utility.toolset.TimeUtils;
import com.motu.vertx.module.utility.toolset.Tool;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
import io.vertx.redis.client.RedisAPI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class WxTool {
    private static final Logger logger = LoggerFactory.getLogger(WxTool.class);

    /**
     * 获取微信小程序的AccessToken
     * @return
     */
    public static Future<String> getAccessToken(WebClient webClient, RedisAPI redisClient,String WX_MINIGAME_APPID,String WX_MINIGAME_APPSECRET,String accessTokenHttp) {
        Promise<String> promise = Promise.promise();
        Exception warnException = new Exception("getAccessToken start!");
        logger.info(Tool.getException(warnException));
        try {
            RedisTools.get(BaseRedisKey.getWxAccesstokenKey(), wxAccesstoken->{
                if (wxAccesstoken.succeeded()){
                    String accessToken = wxAccesstoken.result();
                    if (Tool.isEmpty(accessToken)) {
                        //如果过期为空 从微信 获取新的token
                        Future<String> getTokenFuture = getAccessTokenByWxApi(webClient,redisClient,WX_MINIGAME_APPID,WX_MINIGAME_APPSECRET,accessTokenHttp);
                        getTokenFuture.onComplete(tokenHandler -> {
                            if (tokenHandler.succeeded()) {
                                String result = tokenHandler.result();
                                JsonObject jsonObject = new JsonObject(result);
                                if (!jsonObject.containsKey("errcode")) {
                                    String access_token = jsonObject.getString("access_token");
                                    promise.complete(access_token);
                                }
                            } else {
                                promise.fail(tokenHandler.cause());
                            }
                        });
                    } else {
                        promise.complete(accessToken);
                    }
                }
            });
        } catch (Exception e) {
            logger.error("#### getAccessToken Exception cause = " + Tool.getException(e));
            promise.fail(Tool.getException(e));
        }
        return promise.future();
    }

    /**
     * 从微信API的获取 微信小程序的AccessToken
     * @return
     */
    public static Future<String> getAccessTokenByWxApi(WebClient webClient, RedisAPI redisClient,String WX_MINIGAME_APPID,String WX_MINIGAME_APPSECRET,String accessTokenHttp) {
        Promise<String> promise = Promise.promise();
        try {
            if (!Tool.isEmpty(accessTokenHttp)) {
                //有配置获取地址,从获取地址获取 (TODO:评审服从正式服获取)
                String url = accessTokenHttp + "/wxserver/getAccessToken";
                webClient.getAbs(url).send(h -> {
                    if (h.succeeded()) {
                        HttpResponse<Buffer> response = h.result();
                        JsonObject resObj = new JsonObject(response.bodyAsString());
                        if (!resObj.containsKey("errcode")) {
                            //更新redis
                            String accessToken = resObj.getString("access_token");
                            int accessTokenTime = resObj.getInteger("access_token_time");
                            //access_token 的有效期目前为 2 个小时，需定时刷新，重复获取将导致上次获取的 access_token 失效；所以此处设置1小时50分钟的有效期
                            RedisTools.setAndExpire(BaseRedisKey.getWxAccesstokenKey(), accessToken, accessTokenTime);
                            promise.complete(resObj.toString());
                        } else {
                            //返回的错误格式 {"errcode":40013,"errmsg":"invalid appid"}
                            logger.error("### getAccessTokenByWx SDK resObj:" + resObj);
                            promise.fail(resObj.toString());
                        }
                    } else {
                        logger.error("### getAccessTokenByWx HTTP resObj:" + h.cause());
                        promise.fail(h.cause());
                    }
                });
            } else {
                //从微信api获取
                String appid = WX_MINIGAME_APPID;
                String appsecret = WX_MINIGAME_APPSECRET;
                String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&secret=" + appsecret;
                webClient.getAbs(url).send(h -> {
                    if (h.succeeded()) {
                        HttpResponse<Buffer> response = h.result();
                        JsonObject resObj = new JsonObject(response.bodyAsString());
                        if (resObj.containsKey("errcode")) {
                            //返回的错误格式 {"errcode":40013,"errmsg":"invalid appid"}
                            logger.error("### getAccessTokenByWx SDK resObj:" + resObj);
                            promise.fail(resObj.toString());
                        } else {
                            //更新redis
                            String accessToken = resObj.getString("access_token");
                            //access_token 的有效期目前为 2 个小时，需定时刷新，重复获取将导致上次获取的 access_token 失效；所以此处设置1小时50分钟的有效期
                            RedisTools.setAndExpire(BaseRedisKey.getWxAccesstokenKey(), accessToken, 2 * Tool.HOUR - 10 * Tool.MINUTE);
                            promise.complete(resObj.toString());
                        }
                    } else {
                        logger.error("### getAccessTokenByWx HTTP resObj:" + h.cause());
                        promise.fail(h.cause());
                    }
                });
            }
        } catch (Exception e) {
            logger.error("#### getAccessTokenByWx Exception cause = " + Tool.getException(e));
            promise.fail(Tool.getException(e));
        }
        return promise.future();
    }

    /**
     * 推送微信订阅消息
     * @return
     */
    public static Future<JsonObject> sendWxSubscribe(WebClient webClient, RedisAPI redisClient,String WX_MINIGAME_APPID,String WX_MINIGAME_APPSECRET,String WX_MINIGAME_ACCESSTOKEN_HTTP, String openId, String wxMsgid, JsonObject wxData, long userId) {
        Promise<JsonObject> promise = Promise.promise();
        try{
            //获取微信小程序 AccessToken
            Future<String> accessTokenFuture = getAccessToken(webClient,redisClient,WX_MINIGAME_APPID,WX_MINIGAME_APPSECRET,WX_MINIGAME_ACCESSTOKEN_HTTP);
            accessTokenFuture.onComplete(tokenHandler->{
                if (tokenHandler.succeeded()){
                    String ACCESS_TOKEN = tokenHandler.result();
                    if (!Tool.isEmpty(ACCESS_TOKEN)) {
                        //发起微信订阅消息推送
                        JsonObject params = new JsonObject();
                        params.put("touser", openId);//推送人openId
                        params.put("template_id", wxMsgid);//模版id
                        params.put("page", "index");
                        params.put("data", wxData);
                        String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + ACCESS_TOKEN;
                        webClient.postAbs(url).sendJsonObject(params, h -> {
                            if (h.succeeded()) {
                                HttpResponse<Buffer> response = h.result();
                                JsonObject resObj = response.bodyAsJsonObject();
                                promise.complete(resObj);
                                logger.info("### sendWxSubscribe  resObj:" + resObj  + " taskinfo => userid:" + userId + " msgid : " + wxMsgid);
                            }
                        });
                    }
                }
            });
        }catch (Exception e){
            logger.error("#### sendWxSubscribe Exception cause = " + Tool.getException(e));
            promise.fail(" sendWxSubscribe errro : " +Tool.getException(e));
        }
        return promise.future();
    }

    /**
     * 获取微信用户 session_key
     * @return
     */
    public static Future<String> getWxSessionkey(WebClient webClient, RedisAPI redisClient, String WX_MINIGAME_APPID, String WX_MINIGAME_APPSECRET, String access_token, String openId, String code) {
        Promise<String> promise = Promise.promise();
        try {
            RedisTools.get(BaseRedisKey.getWxUserSessionkey(openId), sessionKeyHandle->{
                if (sessionKeyHandle.succeeded()){
                    String sessionKey = sessionKeyHandle.result();
                    if (!Tool.isEmpty(sessionKey)) {
                        //如果存在 校验是否合法
                        String signature = SHA256_HMAC("",sessionKey);//用户登录态签名
                        String checkUrl = "https://api.weixin.qq.com/wxa/checksession?access_token=" + access_token + "&signature=" + signature + "&openid=" + openId + "&sig_method=hmac_sha256";
                        webClient.getAbs(checkUrl).send(h -> {
                            if (h.succeeded()) {
                                HttpResponse<Buffer> response = h.result();
                                JsonObject resObj = new JsonObject(response.bodyAsString());
                                int errcode = resObj.containsKey("errcode") ? resObj.getInteger("errcode") : 0;
                                if (errcode == 0) {
                                    promise.complete(sessionKey);
                                } else {
                                    //如果已失效, 删除redis 根据code获取最新的
                                    RedisTools.del(BaseRedisKey.getWxUserSessionkey(openId));
                                    Future<String> wxSessionkeyByWxFut = getWxSessionkeyByWx(webClient,WX_MINIGAME_APPID,WX_MINIGAME_APPSECRET,code);
                                    wxSessionkeyByWxFut.onComplete(wxSessionkeyByWxHandler->{
                                        if (wxSessionkeyByWxHandler.succeeded()){
                                            String wxSessionKey = wxSessionkeyByWxHandler.result();
                                            promise.complete(wxSessionKey);
                                        } else{
                                            promise.fail(wxSessionkeyByWxHandler.cause());
                                        }
                                    });
                                }
                            }
                        });
                    } else {
                        //如果不存在 根据code获取最新的
                        Future<String> wxSessionkeyByWxFut = getWxSessionkeyByWx(webClient,WX_MINIGAME_APPID,WX_MINIGAME_APPSECRET,code);
                        wxSessionkeyByWxFut.onComplete(wxSessionkeyByWxHandler->{
                            if (wxSessionkeyByWxHandler.succeeded()){
                                String wxSessionKey = wxSessionkeyByWxHandler.result();
                                promise.complete(wxSessionKey);
                            } else{
                                promise.fail(wxSessionkeyByWxHandler.cause());
                            }
                        });
                    }
                }
            });
        } catch (Exception e) {
            logger.error("#### getWxSessionkey Exception cause = " + Tool.getException(e));
            JsonObject error = new JsonObject();
            error.put("errcode",101);
            error.put("errmsg",Tool.getException(e));
            promise.fail(error.toString());
        }
        return promise.future();
    }

    /**
     * 从微信获取微信用户 session_key
     * @param code
     * @return
     */
    public static Future<String> getWxSessionkeyByWx(WebClient webClient, String WX_MINIGAME_APPID, String WX_MINIGAME_APPSECRET, String code) {
        Promise<String> promise = Promise.promise();
        try {
            if (Tool.isEmpty(code)) {
                JsonObject error = new JsonObject();
                error.put("errcode",102);
                error.put("errmsg","code is empty!");
                promise.fail(error.toString());
            } else {
                String url = "https://api.weixin.qq.com/sns/jscode2session?appid="+WX_MINIGAME_APPID+"&secret="+WX_MINIGAME_APPSECRET+"&js_code="+code+"&grant_type=authorization_code";
                webClient.getAbs(url).send(h -> {
                    if (h.succeeded()) {
                        HttpResponse<Buffer> response = h.result();
                        JsonObject resObj = new JsonObject(response.bodyAsString());
                        int errcode = resObj.containsKey("errcode") ? resObj.getInteger("errcode") : 0;
                        if (errcode == 0) {
                            String session_key = resObj.getString("session_key");
                            promise.complete(session_key);
                        }else {
                            promise.fail(resObj.toString());
                        }
                    } else {
                        logger.error("### getWxSessionkeyByWx error:" + h.cause());
                        promise.fail(h.cause());
                    }
                });
            }
        } catch (Exception e) {
            logger.error("#### getWxSessionkeyByWx Exception cause = " + Tool.getException(e));
            JsonObject error = new JsonObject();
            error.put("errcode",101);
            error.put("errmsg",Tool.getException(e));
            promise.fail(error.toString());
        }
        return promise.future();
    }

    /**
     * 发送微信客服客服消息(目前仅支持发送链接形式)
     */
    public static void sendWxCustomMessage(WebClient webClient, RedisAPI redisClient, String WX_MINIGAME_APPID, String WX_MINIGAME_APPSECRET,String WX_MINIGAME_ACCESSTOKEN_HTTP, String wxOpenid, JsonObject link, JsonObject text){
        try{
            Future<String> tokenFuture = getAccessToken(webClient,redisClient,WX_MINIGAME_APPID,WX_MINIGAME_APPSECRET,WX_MINIGAME_ACCESSTOKEN_HTTP);//获取微信token
            tokenFuture.onComplete(tokenHandler->{
                if (tokenHandler.succeeded()){
                    String ACCESS_TOKEN = tokenHandler.result();
                    if (!Tool.isEmpty(ACCESS_TOKEN)) {
                        String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" + ACCESS_TOKEN;
                        JsonObject params = new JsonObject();
                        params.put("touser", wxOpenid);//推送人id
                        if (link != null) {
                            params.put("msgtype", "link");//图文链接
                            params.put("link",link);
                        } else if (text != null) {
                            params.put("msgtype", "text");//文本消息
                            params.put("text",text);
                        }

                        webClient.postAbs(url).sendJsonObject(params, h -> {
                            if (h.succeeded()) {
                                HttpResponse<Buffer> response = h.result();
                                JsonObject resObj = response.bodyAsJsonObject();
                                logger.info("sendWxCustomMessage resObj:" +resObj + " params:" + params);
                            }
                        });
                    }
                }
            });
        }catch (Exception e){
            logger.error("#### sendWxCustomMessage Exception cause = " + Tool.getException(e));
        }
    }

    /**
     * 检查微信虚拟账户余额
     * @param openId
     * @param wxSessionkey
     */
    public static Future<JsonObject> checkWxBalance(WebClient webClient, RedisAPI redisClient,String app_key,String offer_id,String access_token,String openId , String wxSessionkey,JsonObject orderInfo){
        Promise<JsonObject> promise = Promise.promise();
        try {
            int env = orderInfo.containsKey("env") ? orderInfo.getInteger("env"):0;
            long curTime = TimeUtils.getCurTime();
            JsonObject data = new JsonObject();
            data.put("openid",openId);
            data.put("offer_id",offer_id);//支付应用ID（OfferId）
            data.put("ts",curTime);
            data.put("zone_id","1");
            data.put("env",env);
            String pay_sig = SHA256_HMAC("/wxa/game/getbalance" + "&" + data.toString(),app_key);//支付请求签名
            String signature = SHA256_HMAC(data.toString(),wxSessionkey);//用户登录态签名
            String getBalanceUrl = "https://api.weixin.qq.com/wxa/game/getbalance?access_token="+access_token+"&signature="+signature+"&sig_method=hmac_sha256&pay_sig=" + pay_sig;
            webClient.postAbs(getBalanceUrl).sendJsonObject(data, h -> {
                if (h.succeeded()) {
                    HttpResponse<Buffer> response = h.result();
                    JsonObject resObj = response.bodyAsJsonObject();
                    promise.complete(resObj);
                }else {
                    logger.error("#### checkWxBalance cause = " + h.cause());
                    JsonObject error = new JsonObject();
                    error.put("errcode",101);
                    error.put("errmsg",h.cause());
                    promise.fail(error.toString());
                }
            });
        } catch (Exception e) {
            logger.error("#### checkWxBalance Exception cause = " + Tool.getException(e));
            JsonObject error = new JsonObject();
            error.put("errcode",101);
            error.put("errmsg",Tool.getException(e));
            promise.fail(error.toString());
        }

        return promise.future();
    }

    /**
     * 微信小游戏 虚拟支付扣款操作
     */
    public static void payWxBalance(WebClient webClient, RedisAPI redisClient, String WX_MINIGAME_APPID, String WECHAT_GAME_LOGIN_KEY , String offer_id , String WX_MINIGAME_ACCESSTOKEN_HTTP, String app_key, String openId, String orderid, int amount, Handler<AsyncResult<String>> handler){
        try {
            //获取微信token
            Future<String> tokenFuture = getAccessToken(webClient,redisClient,WX_MINIGAME_APPID,WECHAT_GAME_LOGIN_KEY,WX_MINIGAME_ACCESSTOKEN_HTTP);//获取微信token
            String finalApp_key = app_key;
            tokenFuture.onComplete(tokenHandler->{
                if (tokenHandler.succeeded()){
                    String ACCESS_TOKEN = tokenHandler.result();
                    if (!Tool.isEmpty(ACCESS_TOKEN)) {
                        //获取微信
                        Future<String> wxSessionkeyFut = WxTool.getWxSessionkey(webClient,redisClient,WX_MINIGAME_APPID,WECHAT_GAME_LOGIN_KEY,ACCESS_TOKEN,openId,"");
                        wxSessionkeyFut.onComplete(wxSessionkeyHandler->{
                            if (wxSessionkeyHandler.succeeded()) {
                                String wxSessionkey = wxSessionkeyHandler.result();

                                long curTime = TimeUtils.getCurTime();
                                JsonObject data = new JsonObject();
                                data.put("openid",openId);
                                data.put("offer_id",offer_id);
                                data.put("ts",curTime);
                                data.put("zone_id","1");
                                data.put("env",0);
                                data.put("amount",amount);
                                data.put("bill_no",orderid);

                                String pay_sig = SHA256_HMAC("/wxa/game/pay" + "&" + data.toString(), finalApp_key);//支付请求签名
                                String signature = SHA256_HMAC(data.toString(),wxSessionkey);//用户登录态签名
                                String getBalanceUrl = "https://api.weixin.qq.com/wxa/game/pay?access_token="+ACCESS_TOKEN+"&signature="+signature+"&sig_method=hmac_sha256&pay_sig=" + pay_sig;
                                webClient.postAbs(getBalanceUrl).sendJsonObject(data, h -> {
                                    if (h.succeeded()) {
                                        HttpResponse<Buffer> response = h.result();
                                        JsonObject resObj = response.bodyAsJsonObject();
                                        logger.info("payWxBalance resObj:" + resObj);
                                        if(resObj.getInteger("errcode") == 0) {
                                            handler.handle(Future.succeededFuture("success"));
//                                            promise.complete("success");
                                        }else {
                                            handler.handle(Future.failedFuture(resObj.toString()));
                                        }
                                    }else {
                                        logger.error("#### checkWxBalance cause = " + h.cause());
                                        JsonObject error = new JsonObject();
                                        error.put("errcode",101);
                                        error.put("errmsg",h.cause());
                                        handler.handle(Future.failedFuture(error.toString()));
                                    }
                                });
                            } else {
                                handler.handle(Future.failedFuture(wxSessionkeyHandler.cause().toString()));
                            }
                        });
                    }
                } else {
                    handler.handle(Future.failedFuture(tokenHandler.cause().toString()));
                }
            });
        } catch (Exception e) {
            logger.error("#### checkWxBalance Exception cause = " + Tool.getException(e));
            JsonObject error = new JsonObject();
            error.put("errcode",101);
            error.put("errmsg",Tool.getException(e));
            handler.handle(Future.failedFuture(error.toString()));
        }
    }

    /**
     * 文本内容安全识别
     */
    public static Future<Boolean> msgSecCheck(WebClient webClient, RedisAPI redisClient, String WX_MINIGAME_APPID,String WECHAT_GAME_LOGIN_KEY,String WX_MINIGAME_ACCESSTOKEN_HTTP, String openId, String content){
        Promise<Boolean> promise = Promise.promise();
        try{
            //获取微信小程序 AccessToken
            Future<String> accessTokenFuture = getAccessToken(webClient,redisClient,WX_MINIGAME_APPID,WECHAT_GAME_LOGIN_KEY,WX_MINIGAME_ACCESSTOKEN_HTTP);
            accessTokenFuture.onComplete(tokenHandler->{
                if (tokenHandler.succeeded()){
                    String ACCESS_TOKEN = tokenHandler.result();
                    if (!Tool.isEmpty(ACCESS_TOKEN)) {
                        //发起微信订阅消息推送
                        JsonObject params = new JsonObject();
                        params.put("openid", openId);//openId
                        params.put("scene", 1);//场景枚举值（1 资料；2 评论；3 论坛；4 社交日志）
                        params.put("version", 2);//接口版本号，2.0版本为固定值2
                        params.put("content", content);
                        String url = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + ACCESS_TOKEN;
                        System.out.println("msgSecCheck ACCESS_TOKEN:" + ACCESS_TOKEN + " params:" + params);
                        webClient.postAbs(url).timeout(5000).sendJsonObject(params, h -> {
                            if (h.succeeded()) {
                                HttpResponse<Buffer> response = h.result();
                                JsonObject resObj = response.bodyAsJsonObject();
                                int errcode = resObj.getInteger("errcode");
                                if (errcode == 0){
                                    // 请求成功
                                    if (resObj.getJsonObject("result").getInteger("label") == 100){
                                        // 内容校验正常
                                        System.out.println("msgSecCheck success result:" + resObj);
                                        promise.complete(true);
                                    } else{
                                        // 内容校验不通过
                                        logger.warn("msgSecCheck label error content:{} result:{}",content, resObj);
                                        promise.complete(false);
                                    }
                                } else{
                                    // 请求返回错误码
                                    /**
                                     * -1 系统繁忙，此时请开发者稍候再试
                                     * 44991 超出接口每分钟调用限制
                                     * 45009 超出接口每日调用限制
                                     */
                                    logger.warn("msgSecCheck code error content:{} result:{}",content, resObj);
                                    if (errcode == -1 || errcode == 44991 || errcode == 45009){
                                        // 如果是系统原因，或者是超过限制，则让验证通过
                                        promise.complete(true);
                                    } else{
                                        promise.complete(false);
                                    }
                                }
                            } else{
                                logger.error("msgSecCheck error content:{} exception:{}",content, Tool.getException(h.cause()));
                                promise.complete(false);
                            }
                        });
                    } else{
                        logger.error("msgSecCheck error token null content:{}",content);
                        promise.complete(false);
                    }
                } else{
                    logger.error("msgSecCheck error get token fail content:{} exception:{}",content, Tool.getException(tokenHandler.cause()));
                    promise.complete(false);
                }
            });
        }catch (Exception e){
            logger.error("msgSecCheck error content:{} exception:{}",content, Tool.getException(e));
            promise.complete(false);
        }
        return promise.future();
    }

    /**
     * sha256_HMAC加密
     * @param message 消息
     * @param secret  秘钥
     * @return 加密后字符串
     */
    public static String SHA256_HMAC(String message, String secret) {
        String hash = "";
        try {
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
            sha256_HMAC.init(secret_key);
            byte[] bytes = sha256_HMAC.doFinal(message.getBytes());
            hash = byteArrayToHexString(bytes);
        } catch (Exception e) {
            logger.error("Error HmacSHA256 ===========" + e.getMessage());
        }
        return hash;
    }

    /**
     * 将加密后的字节数组转换成字符串
     * @param b 字节数组
     * @return 字符串
     */
    public  static String byteArrayToHexString(byte[] b) {
        StringBuilder hs = new StringBuilder();
        String stmp;
        for (int n = 0; b!=null && n < b.length; n++) {
            stmp = Integer.toHexString(b[n] & 0XFF);
            if (stmp.length() == 1)
                hs.append('0');
            hs.append(stmp);
        }
        return hs.toString().toLowerCase();
    }

    public static String AEAD_AES_256_GCM(String apiV3key,String msg,String nonce ,String associated_data) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            byte[] decode = Base64.getDecoder().decode(msg);

            cipher.init(2, new SecretKeySpec(apiV3key.getBytes(), "AES"), new GCMParameterSpec(128, nonce.getBytes()));
            if (!Tool.isEmpty(associated_data)) {
                cipher.updateAAD(associated_data.getBytes());
            }
            String str = new String(cipher.doFinal(decode), StandardCharsets.UTF_8);
            return str;
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

}
